// import * as THREE from 'three';

import { $set } from '@feng3d/serialization';
import { CameraHelper3D, CatmullRomCurve3, Color, Mesh3D, MeshBasicMaterial3D, MeshLambertMaterial3D, Node3D, PerspectiveCamera3D, Scene3D, SphereGeometry3D, StatsComponent, TubeGeometry3D, Vector3, WebGLRenderer3D } from 'feng3d-three';
import GUI from 'lil-gui';
import '../../extension/controls/OrbitControls3D';
import { Curves } from '../../extension/thirdparty/three/imports';

// import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
// import Stats from 'three/addons/libs/stats.module.js';

// import * as Curves from 'three/addons/curves/CurveExtras.js';

let container: HTMLElement;
let stats: StatsComponent;

let camera: PerspectiveCamera3D;
let scene: Scene3D;
let renderer: WebGLRenderer3D;
let splineCamera: PerspectiveCamera3D;
let cameraHelper: CameraHelper3D;
let cameraEye: Mesh3D;

const direction = new Vector3();
const binormal = new Vector3();
const normal = new Vector3();
const position = new Vector3();
const lookAt = new Vector3();

const pipeSpline = new CatmullRomCurve3([
    new Vector3(0, 10, -10), new Vector3(10, 0, -10),
    new Vector3(20, 0, 0), new Vector3(30, 0, 10),
    new Vector3(30, 0, 20), new Vector3(20, 0, 30),
    new Vector3(10, 0, 30), new Vector3(0, 0, 30),
    new Vector3(-10, 10, 30), new Vector3(-10, 20, 30),
    new Vector3(0, 30, 30), new Vector3(10, 30, 30),
    new Vector3(20, 30, 15), new Vector3(10, 30, 10),
    new Vector3(0, 30, 10), new Vector3(-10, 20, 10),
    new Vector3(-10, 10, 10), new Vector3(0, 0, 10),
    new Vector3(10, -10, 10), new Vector3(20, -15, 10),
    new Vector3(30, -15, 10), new Vector3(40, -15, 10),
    new Vector3(50, -15, 10), new Vector3(60, 0, 10),
    new Vector3(70, 0, 0), new Vector3(80, 0, 0),
    new Vector3(90, 0, 0), new Vector3(100, 0, 0)
]);

const sampleClosedSpline = new CatmullRomCurve3([
    new Vector3(0, -40, -40),
    new Vector3(0, 40, -40),
    new Vector3(0, 140, -40),
    new Vector3(0, 40, 40),
    new Vector3(0, -40, 40)
]);

sampleClosedSpline.curveType = 'catmullrom';
sampleClosedSpline.closed = true;

// Keep a dictionary of Curve instances
const splines = {
    GrannyKnot: new Curves.GrannyKnot(),
    HeartCurve: new Curves.HeartCurve(3.5),
    VivianiCurve: new Curves.VivianiCurve(70),
    KnotCurve: new Curves.KnotCurve(),
    HelixCurve: new Curves.HelixCurve(),
    TrefoilKnot: new Curves.TrefoilKnot(),
    TorusKnot: new Curves.TorusKnot(20),
    CinquefoilKnot: new Curves.CinquefoilKnot(20),
    TrefoilPolynomialKnot: new Curves.TrefoilPolynomialKnot(14),
    FigureEightPolynomialKnot: new Curves.FigureEightPolynomialKnot(),
    DecoratedTorusKnot4a: new Curves.DecoratedTorusKnot4a(),
    DecoratedTorusKnot4b: new Curves.DecoratedTorusKnot4b(),
    DecoratedTorusKnot5a: new Curves.DecoratedTorusKnot5a(),
    DecoratedTorusKnot5c: new Curves.DecoratedTorusKnot5c(),
    PipeSpline: pipeSpline,
    SampleClosedSpline: sampleClosedSpline
};

let parent: Node3D;
let tubeGeometry: TubeGeometry3D;
let mesh: Mesh3D;

const params = {
    spline: 'GrannyKnot',
    scale: 4,
    extrusionSegments: 100,
    radiusSegments: 3,
    closed: true,
    animationView: false,
    lookAhead: false,
    cameraHelper: false,
};

const material = $set(new MeshLambertMaterial3D(), { color: { value: 0xff00ff } });

const wireframeMaterial = $set(new MeshBasicMaterial3D(), { color: { value: 0x000000 }, opacity: 0.3, wireframe: true, transparent: true });

function addTube()
{
    if (mesh !== undefined)
    {
        parent.removeChild(mesh.entity);
        mesh.geometry.dispose();
    }

    const extrudePath = splines[params.spline];

    tubeGeometry = $set(new TubeGeometry3D(), {
        path: extrudePath, tubularSegments: params.extrusionSegments, radius: 2,
        radiusSegments: params.radiusSegments, closed: params.closed
    });

    addGeometry(tubeGeometry);

    setScale();
}

function setScale()
{
    mesh.entity.scale.set(params.scale, params.scale, params.scale);
}

function addGeometry(geometry: TubeGeometry3D)
{
    // 3D shape

    mesh = new Node3D().addComponent('Mesh3D', { geometry, material });
    const wireframe = new Node3D().addComponent('Mesh3D', { geometry, material: wireframeMaterial });
    mesh.entity.addChild(wireframe.entity);

    parent.addChild(mesh.entity);
}

function animateCamera()
{
    cameraHelper.entity.visible = params.cameraHelper;
    cameraEye.entity.visible = params.cameraHelper;
}

init();
animate();

function init()
{
    container = document.getElementById('container');

    // camera

    camera = new Node3D().addComponent('PerspectiveCamera3D', {
        fov: 50, aspect: window.innerWidth / window.innerHeight, near: 0.01, far: 10000
    });
    camera.entity.position.set(0, 50, 500);

    // scene

    scene = new Node3D().addComponent('Scene3D');
    scene.background = new Color(0xf0f0f0);

    // light

    const light = new Node3D().addComponent('DirectionalLight3D', { color: { value: 0xffffff } });
    light.entity.position.set(0, 0, 1);
    scene.entity.addChild(light.entity);

    // tube

    parent = new Node3D();
    scene.entity.addChild(parent);

    splineCamera = new Node3D().addComponent('PerspectiveCamera3D', {
        fov: 84, aspect: window.innerWidth / window.innerHeight, near: 0.01, far: 1000
    });
    parent.addChild(splineCamera.entity);

    cameraHelper = new Node3D().addComponent('CameraHelper3D', { camera: splineCamera });
    scene.entity.addChild(cameraHelper.entity);

    addTube();

    // debug camera

    cameraEye = new Node3D().addComponent('Mesh3D', {
        geometry: $set(new SphereGeometry3D(), { radius: 5 }),
        material: $set(new MeshBasicMaterial3D(), { color: { value: 0xdddddd } })
    });
    parent.addChild(cameraEye.entity);

    cameraHelper.entity.visible = params.cameraHelper;
    cameraEye.entity.visible = params.cameraHelper;

    // renderer

    renderer = scene.addComponent('WebGLRenderer3D', { parameters: { antialias: true } });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    // stats

    stats = scene.addComponent('StatsComponent');
    container.appendChild(stats.dom);

    // dat.GUI

    const gui = new GUI({ width: 285 });

    const folderGeometry = gui.addFolder('Geometry');
    folderGeometry.add(params, 'spline', Object.keys(splines)).onChange(function ()
    {
        addTube();
    });
    folderGeometry.add(params, 'scale', 2, 10).step(2).onChange(function ()
    {
        setScale();
    });
    folderGeometry.add(params, 'extrusionSegments', 50, 500).step(50).onChange(function ()
    {
        addTube();
    });
    folderGeometry.add(params, 'radiusSegments', 2, 12).step(1).onChange(function ()
    {
        addTube();
    });
    folderGeometry.add(params, 'closed').onChange(function ()
    {
        addTube();
    });
    folderGeometry.open();

    const folderCamera = gui.addFolder('Camera');
    folderCamera.add(params, 'animationView').onChange(function ()
    {
        animateCamera();
    });
    folderCamera.add(params, 'lookAhead').onChange(function ()
    {
        animateCamera();
    });
    folderCamera.add(params, 'cameraHelper').onChange(function ()
    {
        animateCamera();
    });
    folderCamera.open();

    const controls = scene.addComponent('OrbitControls3D', { camera, domElement: renderer.domElement });
    controls.minDistance = 100;
    controls.maxDistance = 2000;

    window.addEventListener('resize', onWindowResize);
}

function onWindowResize()
{
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();

    renderer.setSize(window.innerWidth, window.innerHeight);
}

//

function animate()
{
    requestAnimationFrame(animate);

    render();
    stats.update();
}

function render()
{
    // animate camera along spline

    const time = Date.now();
    const looptime = 20 * 1000;
    const t = (time % looptime) / looptime;

    tubeGeometry.parameters.path.getPointAt(t, position);
    position.multiplyScalar(params.scale);

    // interpolation

    const segments = tubeGeometry.tangents.length;
    const pickt = t * segments;
    const pick = Math.floor(pickt);
    const pickNext = (pick + 1) % segments;

    binormal.subVectors(tubeGeometry.binormals[pickNext], tubeGeometry.binormals[pick]);
    binormal.multiplyScalar(pickt - pick).add(tubeGeometry.binormals[pick]);

    tubeGeometry.parameters.path.getTangentAt(t, direction);
    const offset = 15;

    normal.copy(binormal).cross(direction);

    // we move on a offset on its binormal

    position.add(normal.clone().multiplyScalar(offset));

    splineCamera.entity.position.copy(position);
    cameraEye.entity.position.copy(position);

    // using arclength for stablization in look ahead

    tubeGeometry.parameters.path.getPointAt((t + 30 / tubeGeometry.parameters.path.getLength()) % 1, lookAt);
    lookAt.multiplyScalar(params.scale);

    // camera orientation 2 - up orientation via normal

    if (!params.lookAhead) lookAt.copy(position).add(direction);
    splineCamera.entity.matrix.lookAt(splineCamera.entity.position, lookAt, normal);
    splineCamera.entity.quaternion.setFromRotationMatrix(splineCamera.entity.matrix);

    cameraHelper.update();

    renderer.render(scene, params.animationView === true ? splineCamera : camera);
}
